﻿// (c) Norbert Huffschmid
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Net;
using NProtocol;
using NProtocol.Codec;
using NProtocol.Rtp.Rtcp;
using NProtocol.Rtp.Stack;
using NProtocol.ShoutBoxControlProtocol;
using NProtocol.ShoutBoxDiscoveryProtocol;

namespace ShoutBox.Core
{
    /// <summary>
    /// Shoutbox endpoint
    /// </summary>
    public class ShoutBoxEndPoint : IShoutBox
    {
#if LOG4NET
        private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
#endif

        /// <summary>
        /// Gets the shoutbox ID
        /// </summary>
        public Guid ShoutBoxId
        {
            get { return this.shoutBoxId; }
        }

        /// <summary>
        /// Gets all available session members
        /// </summary>
        public ObservableCollection<ISessionMember> SessionMembers
        {
            get { return this.sessionMembers; }
        }

        /// <summary>
        /// Gets or sets the endpoint name
        /// </summary>
        public string Name
        {
            get { return this.rtpEndSystem.Name; }

            set
            {
#if LOG4NET
                log.Info("Set name: " + value);
#endif
                this.rtpEndSystem.Name = value;
            }
        }


        private int rtpPort;
        private int rtcpPort;
        private Guid shoutBoxId;
        private int shoutBoxDiscoveryPort;
        private IPAddress shoutBoxDiscoveryMulticastGroup;
        private int shoutBoxDiscoveryInterval;
        private IPlatformAdapterAudioOut platformAdapter;
        private RtpEndSystem rtpEndSystem;
        private ObservableCollection<ISessionMember> sessionMembers;
        private Transcoder aLawAudioTranscoder;
        private Transcoder muLawAudioTranscoder;
        private Transcoder sendTranscoder;
        private Mixer mixer;
        private ShoutBoxControlMessageFactory shoutBoxControlMessageFactory;
        private ShoutBoxDiscoveryClient shoutBoxDiscoveryClient;
        private bool disposed = false;


        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="shoutBoxDiscoveryPort">Listening ShoutBoxDiscovery port</param>
        /// <param name="shoutBoxDiscoveryMulticastAddress">Multicast IP address of Shoutbox discovery server</param>
        /// <param name="shoutBoxDiscoveryInterval">Shoutbox discovery interval</param>
        /// <param name="platformAdapter">Platform adapter</param>
        public ShoutBoxEndPoint(int shoutBoxDiscoveryPort, IPAddress shoutBoxDiscoveryMulticastIPAddress, int shoutBoxDiscoveryInterval, IPlatformAdapterAudioOut platformAdapter)
            :this(0, 0, shoutBoxDiscoveryPort, shoutBoxDiscoveryMulticastIPAddress, shoutBoxDiscoveryInterval, platformAdapter) { }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="rtpPort">Listening RTP port</param>
        /// <param name="rtcpPort">Listening RTCP port</param>
        /// <param name="shoutBoxDiscoveryPort">Listening ShoutBoxDiscovery port</param>
        /// <param name="shoutBoxDiscoveryMulticastGroup">Multicast IP address of Shoutbox discovery server</param>
        /// <param name="shoutBoxDiscoveryInterval">Shoutbox discovery interval</param>
        /// <param name="platformAdapter">Platform adapter</param>
        public ShoutBoxEndPoint(int rtpPort, int rtcpPort, int shoutBoxDiscoveryPort,
            IPAddress shoutBoxDiscoveryMulticastGroup, int shoutBoxDiscoveryInterval, IPlatformAdapterAudioOut platformAdapter)
        {
#if LOG4NET
            log.Info("Constructor - shoutBoxDiscoveryMulticastGroup: " + shoutBoxDiscoveryMulticastGroup.ToString() +
                     " - Ports: rtp: " + rtpPort + ", rtcp: " + rtcpPort + ", shoutBoxDiscovery: " + shoutBoxDiscoveryPort +
                     " - shoutBoxDiscoveryInterval: " + shoutBoxDiscoveryInterval + " msec");
#endif
            this.rtpPort = rtpPort;
            this.rtcpPort = rtcpPort;
            this.shoutBoxId = Guid.NewGuid();
            this.shoutBoxDiscoveryPort = shoutBoxDiscoveryPort;
            this.shoutBoxDiscoveryMulticastGroup = shoutBoxDiscoveryMulticastGroup;
            this.shoutBoxDiscoveryInterval = shoutBoxDiscoveryInterval;
            this.platformAdapter = platformAdapter;
            this.mixer = new Mixer(this.platformAdapter);

            this.sessionMembers = new ObservableCollection<ISessionMember>();

            this.aLawAudioTranscoder = new Transcoder(RealTimeDataFormat.Audio_Pcm_ALaw, RealTimeDataFormat.Audio_8kHz_16Bit_Mono);
            this.muLawAudioTranscoder = new Transcoder(RealTimeDataFormat.Audio_Pcm_MuLaw, RealTimeDataFormat.Audio_8kHz_16Bit_Mono);
            this.sendTranscoder = new Transcoder(RealTimeDataFormat.Audio_8kHz_16Bit_Mono, RealTimeDataFormat.Audio_Pcm_MuLaw);

            this.shoutBoxControlMessageFactory = new ShoutBoxControlMessageFactory();
        }


        public void StartNetworkServices()
        {
#if LOG4NET
            log.Info("StartNetworkServices");
#endif
            if (this.rtpEndSystem == null)
            {
                this.rtpEndSystem = new RtpEndSystem(this.platformAdapter.ShoutBoxName, this.rtcpPort, this.rtpPort);
                this.rtpEndSystem.RealTimeDataReceived +=
                    new RealTimeDataReceivedEventHandler(rtpEndSystem_RealTimeDataReceived);
                this.rtpEndSystem.ApplicationDefinedPacketReceived +=
                    new ApplicationDefinedPacketReceivedEventHandler(rtpEndSystem_ApplicationDefinedPacketReceived);
                this.rtpEndSystem.SessionMembers.CollectionChanged +=
                    new NotifyCollectionChangedEventHandler(SessionMembers_CollectionChanged);

#if WINDOWS_PHONE && DEBUG
                // IP multicast does not work in the Windows Phone debugger ==> simulate discovery
                this.rtpEndSystem.InviteEndPoint(System.Net.IPAddress.Parse("192.168.178.25"), 14647, 14646);
                this.rtpEndSystem.InviteEndPoint(System.Net.IPAddress.Parse("192.168.178.35"), 14647, 14646);
#else
                this.shoutBoxDiscoveryClient = new ShoutBoxDiscoveryClient(shoutBoxDiscoveryMulticastGroup,
                    shoutBoxDiscoveryPort, shoutBoxDiscoveryInterval);

                this.shoutBoxDiscoveryClient.ShoutBoxEndPointDetected +=
                    new ShoutBoxEndPointDetectedEventHandler(shoutBoxDiscoveryClient_ShoutBoxEndPointDetected);
#endif
            }
        }

        public void StopNetworkServices()
        {
#if LOG4NET
            log.Info("StopNetworkServices");
#endif
            this.sessionMembers.Clear();

            if (this.rtpEndSystem != null)
            {
                this.rtpEndSystem.Dispose();
                this.rtpEndSystem = null;
            }

            if (this.shoutBoxDiscoveryClient != null)
            {
                this.shoutBoxDiscoveryClient.Dispose();
                this.shoutBoxDiscoveryClient = null;
            }
        }

        /// <summary>
        /// Start shout procedure
        /// </summary>
        /// <param name="target">Target of shout operation</param>
        public void StartShout(ISessionMember target)
        {
#if LOG4NET
            if (target != null)
                log.Info("StartShout - target: " + target.Name);
            else
                log.Info("StartShout - target: null");
#endif
            RtpSessionMember shoutTarget = target as RtpSessionMember;

            if (shoutTarget != null)
            {
                // send application-defined RTCP message for StartOfShout
                StartOfShoutMessage startOfShoutMessage = new StartOfShoutMessage();
                ApplicationDefinedRtcpPacket appPacket = new ApplicationDefinedRtcpPacket();
                appPacket.SsrcCsrc = this.rtpEndSystem.Ssrc;
                appPacket.Name = ShoutBoxControlMessageFactory.APPLICATION_DEFINED_RTCP_PACKET_NAME;
                appPacket.Data = startOfShoutMessage.Encode();
                this.rtpEndSystem.SendApplicationDefinedRtcpPacket(appPacket, shoutTarget);

                // reset volume info
                shoutTarget.Tag = null;
            }
        }

        /// <summary>
        /// Terminate shout procedure
        /// </summary>
        /// <param name="target">Target of EndOfShout operation</param>
        public void EndOfShout(ISessionMember target)
        {
#if LOG4NET
            if (target != null)
                log.Info("EndOfShout - target: " + target.Name);
            else
                log.Info("EndOfShout - target: null");
#endif
            RtpSessionMember shoutTarget = target as RtpSessionMember;

            if (shoutTarget != null)
            {
                // send application-defined RTCP message for EndOfShout
                EndOfShoutMessage endOfShoutMessage = new EndOfShoutMessage();
                ApplicationDefinedRtcpPacket appPacket = new ApplicationDefinedRtcpPacket();
                appPacket.SsrcCsrc = this.rtpEndSystem.Ssrc;
                appPacket.Name = "SBox";
                appPacket.Data = endOfShoutMessage.Encode();
                this.rtpEndSystem.SendApplicationDefinedRtcpPacket(appPacket, shoutTarget);
            }
        }

        /// <summary>
        /// Set shout volume
        /// </summary>
        /// <param name="target">Target of SetShoutVolume operation</param>
        /// <param name="volume">Shout volume</param>
        public void SetShoutVolume(ISessionMember target, UInt16 volume)
        {
#if LOG4NET
            if (target != null)
                log.Debug("SetShoutVolume - target: " + target.Name + ", volume: " + volume.ToString());
            else
                log.Debug("SetShoutVolume - target: null, volume: " + volume.ToString());
#endif
            RtpSessionMember shoutTarget = target as RtpSessionMember;

            if (shoutTarget != null)
            {
                if ((!(shoutTarget.Tag is UInt16)) || ((UInt16)shoutTarget.Tag != volume))
                {
                    // send application-defined RTCP message for Volume
                    VolumeMessage volumeMessage = new VolumeMessage();
                    volumeMessage.Volume = volume;
                    ApplicationDefinedRtcpPacket appPacket = new ApplicationDefinedRtcpPacket();
                    appPacket.Name = "SBox";
                    appPacket.Data = volumeMessage.Encode();
                    this.rtpEndSystem.SendApplicationDefinedRtcpPacket(appPacket, shoutTarget);

                    // send next VolumeMessage only if volume has changed
                    shoutTarget.Tag = volume;
                }
            }
        }

        /// <summary>
        /// Stream shout audio data to remote target
        /// </summary>
        /// <param name="target">Target of SetShoutVolume operation</param>
        /// <param name="buffer">Audio buffer</param>
        /// <param name="format">Data format</param>
        public void StreamShout(ISessionMember target, byte[] buffer, RealTimeDataFormat format)
        {
#if LOG4NET
            if (target != null)
                log.Debug("StreamShout - target: " + target.Name + ", buffer: " + buffer.Length + " bytes, format: " + format.ToString());
            else
                log.Debug("StreamShout - target: null, buffer: " + buffer.Length + " bytes, format: " + format.ToString());
#endif
            RtpSessionMember shoutTarget = target as RtpSessionMember;

            if (shoutTarget != null)
            {
                if (sendTranscoder.FromFormat != format)
                    throw new ArgumentException("Invalid data format");

                if (shoutTarget.PayloadEndPoint != null)
                {
                    byte[] transcodedAudio = this.sendTranscoder.Transcode(buffer);

                    try
                    {
                        this.rtpEndSystem.SendRealTimeData(transcodedAudio,
                            RealTimeDataFormat.Audio_Pcm_MuLaw, shoutTarget);
                    }
                    catch (Exception ex)
                    {
#if LOG4NET
                        log.Error(ex.Message, ex);
#else
                        System.Diagnostics.Debug.WriteLine(ex.Message);
#endif
                    }
                }
            }
        }

        /// <summary>
        /// Release all resources
        /// </summary>
        public void Dispose()
        {
#if LOG4NET
            log.Debug("Dispose");
#endif
            Dispose(true);
            GC.SuppressFinalize(this);
        }


        protected virtual void Dispose(bool disposing)
        {
            if (!disposed)
            {
                if (disposing)
                {
                    if (this.rtpEndSystem != null)
                        this.rtpEndSystem.Dispose();

                    if (this.shoutBoxDiscoveryClient != null)
                        this.shoutBoxDiscoveryClient.Dispose();

                    this.mixer.Dispose();
                }
            }
            disposed = true;
        }


        private void shoutBoxDiscoveryClient_ShoutBoxEndPointDetected(object sender, ShoutBoxEndPointInfoEventArgs e)
        {
#if LOG4NET
            log.Debug("ShoutBox endpoint detected: IP address: " + e.IPAddress.ToString() +
                ", RTCP/RTP port: " + e.DiscoveryResponseDatagram.RtcpPort + "/" + e.DiscoveryResponseDatagram.RtpPort);
#endif

#if !DEBUG
            if (e.DiscoveryResponseDatagram.ShoutBoxId == this.shoutBoxId)
                return; // ignore out own responses
#endif
            // determine if this is a already known shoutbox
            bool isNewShoutBox = true;
            foreach (RtpSessionMember rtpSessionMember in this.sessionMembers)
            {
                if (rtpSessionMember.Address.Equals(e.IPAddress))
                {
                    isNewShoutBox = false; // already known
                    break;
                }
            }

            if (isNewShoutBox)
            {
                // invite detected shoutbox to join RTP session
#if LOG4NET
                log.Info("Invite new shoutbox: IP address: " + e.IPAddress.ToString() +
                    ", RTCP/RTP port: " + e.DiscoveryResponseDatagram.RtcpPort + "/" + e.DiscoveryResponseDatagram.RtpPort);
#endif
                this.rtpEndSystem.InviteEndPoint(e.IPAddress,
                    e.DiscoveryResponseDatagram.RtcpPort, e.DiscoveryResponseDatagram.RtpPort);
            }
        }

        private void SessionMembers_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
#if LOG4NET
            log.Debug("SessionMembers_CollectionChanged - action: " + e.Action.ToString());
#endif
            if (e.Action == NotifyCollectionChangedAction.Add)
            {
                foreach (RtpSessionMember rtpSessionMember in e.NewItems)
                {
#if LOG4NET
                    log.Info("Add RtpSessionMember: " + rtpSessionMember.ToString());
#endif
                    this.sessionMembers.Add(rtpSessionMember);

                    // send audio ping to new member to deliver our RTP port address
                    if (rtpSessionMember.PayloadEndPoint != null)
                    {
#if LOG4NET
                        log.Debug("Sending audio ping to added session member.");
#endif
                        byte[] pingData = new byte[0];
                        this.rtpEndSystem.SendRealTimeData(pingData,
                            RealTimeDataFormat.Audio_Pcm_MuLaw, rtpSessionMember);
                    }
                }
            }
            else if (e.Action == NotifyCollectionChangedAction.Remove)
            {
                foreach (RtpSessionMember rtpSessionMember in e.OldItems)
                {
#if LOG4NET
                    log.Info("Remove RtpSessionMember: " + rtpSessionMember.ToString());
#endif
                    this.sessionMembers.Remove(rtpSessionMember);

                    // needed to remove pending shout indication
                    rtpSessionMember.IsSender = false;
                }
            }
            else if (e.Action == NotifyCollectionChangedAction.Reset)
            {
#if LOG4NET
                log.Info("Reset all RtpSessionMembers");
#endif
                this.sessionMembers.Clear();
            }
        }

        private void rtpEndSystem_RealTimeDataReceived(object sender, RealTimeDataEventArgs e)
        {
#if LOG4NET
            log.Debug("RealTimeDataReceived: " + e.Buffer.Length + " bytes");
#endif
            byte[] pcmData = null;

            if (e.RealTimeDataFormat == RealTimeDataFormat.Audio_Pcm_MuLaw)
            {
                pcmData = this.muLawAudioTranscoder.Transcode(e.Buffer);
            }
            else if (e.RealTimeDataFormat == RealTimeDataFormat.Audio_Pcm_ALaw)
            {
                pcmData = this.aLawAudioTranscoder.Transcode(e.Buffer);
            }

            if ((pcmData != null) && (pcmData.Length > 0))
                this.mixer.PlayPcmData(e.Sender, pcmData);
        }

        private void rtpEndSystem_ApplicationDefinedPacketReceived(object sender, RtpApplicationDefinedEventArgs e)
        {
#if LOG4NET
            log.Debug("ApplicationDefinedPacketReceived: " + e.Packet.ToString());
#endif
            if (e.Packet.Name.Equals(ShoutBoxControlMessageFactory.APPLICATION_DEFINED_RTCP_PACKET_NAME))
            {
                try
                {
                    // TODO: handle shouts from multiple sources

                    ShoutBoxControlMessage shoutBoxMessage =
                        (ShoutBoxControlMessage)this.shoutBoxControlMessageFactory.CreateDatagram(e.Packet.Data);
#if LOG4NET
                    log.Info("ShoutBoxControlMessage received: " + shoutBoxMessage.ToString());
#endif

                    if (shoutBoxMessage is StartOfShoutMessage)
                    {
                        StartOfShoutMessage startOfShoutMessage = (StartOfShoutMessage)shoutBoxMessage;

                        // save current volume level
                        this.platformAdapter.SaveVolumeLevel();
                    }
                    else if (shoutBoxMessage is EndOfShoutMessage)
                    {
                        // restore original volume level
                        this.platformAdapter.RestoreVolumeLevel();

                        // accelerate rtp isSender detection
                        if (e.PacketSender != null)
                            e.PacketSender.IsSender = false;
                    }
                    else if (shoutBoxMessage is VolumeMessage)
                    {
                        VolumeMessage volumeMessage = (VolumeMessage)shoutBoxMessage;

                        // set volume
                        this.platformAdapter.SetVolumeLevel((double)volumeMessage.Volume / UInt16.MaxValue);
                    }
                }
                catch (ShoutBoxControlProtocolException ex)
                {
                    // silently discard unsupported message, e.g. from higher protocol version
#if LOG4NET
                    log.Warn(ex.Message, ex);
#else
                    System.Diagnostics.Debug.WriteLine(ex.Message);
#endif
                }
            }
        }
    }
}
